package pl.edu.agh.nn.kohonen;

import java.util.ArrayList;
import java.util.List;

import pl.edu.agh.nn.conf.InvalidNetworkConfigurationException;
import pl.edu.agh.nn.kohonen.distance.Distance;
import pl.edu.agh.nn.kohonen.distance.EuclideanDistance;
import pl.edu.agh.nn.net.AbstractNetWithConfiguration;

public class KohonenNet extends AbstractNetWithConfiguration {

	private KohonenConfiguration kohonenConfiguration;
	private List<KohonenNeuron> neurons;
	private Distance distance;
	private double learningRate;
	private double neighbourhood;
	private int currentStep;

	public KohonenNet() {
	}

	public KohonenNet(KohonenConfiguration kohonenConfiguration) {
		this.kohonenConfiguration = kohonenConfiguration;
	}

	@Override
	public double[] compute(double[] input) {
		double results[] = new double[getNeurons().size()];
		for (int i = 0; i < getNeurons().size(); i++) {
			results[i] = 0;
		}

		double bestFitness = Double.MAX_VALUE;
		int bestNeuron = 0;
		for (int i = 0; i < getNeurons().size(); i++) {
			double fitness = getNeurons().get(i).computeFitness(input, distance);
			if (fitness < bestFitness) {
				bestFitness = fitness;
				bestNeuron = i;
			}
		}

		results[bestNeuron] = 1;
		return results;
	}

	@Override
	public double[] learn(double[] inputs, double[] output) {
		++currentStep;

		double bestFitness = 1000000000.0;
		KohonenNeuron bestNeuron = null;

		for (KohonenNeuron neuron : getNeurons()) {
			neuron.increaseConscience();
			if (neuron.canCompete()) {
				double fitness = neuron.computeFitness(inputs, distance);
				if (fitness < bestFitness) {
					bestFitness = fitness;
					bestNeuron = neuron;
				}
			}
		}

		bestNeuron.decreaseConscience();
		bestNeuron.denoteWin();

		bestNeuron.pullNetworkTowards(inputs);
		learningRate *= kohonenConfiguration.getLearningRateReduction();
		neighbourhood *= kohonenConfiguration.getNeighbourhoodReduction();

		return compute(inputs);
	}

	@Override
	public void configure() throws InvalidNetworkConfigurationException {
		if (ioConfiguration != null) {
			ioConfiguration.configure();
		}
		currentStep = 0;
		setNeurons(new ArrayList<KohonenNeuron>());
		learningRate = kohonenConfiguration.getInitialLearningRate();
		neighbourhood = kohonenConfiguration.getInitialNeighbourhood();
		if (distance == null) {
			distance = new EuclideanDistance();
		}
		for (int i = 0; i < kohonenConfiguration.getPlaneWidth() * kohonenConfiguration.getPlaneHeight(); ++i) {
			getNeurons().add(new KohonenNeuron(this));
		}
		calculateNeighbours();
	}

	public double getCurrentLearningRate() {
		return learningRate;
	}

	public double getCurrentNeighbourhood() {
		return neighbourhood;
	}

	private void calculateNeighbours() {
		if (!kohonenConfiguration.getUseZigZagConnections()) {
			for (int x = 0; x < kohonenConfiguration.getPlaneWidth(); ++x) {
				for (int y = 0; y < kohonenConfiguration.getPlaneHeight(); ++y) {
					KohonenNeuron c = getNeurons().get(x * kohonenConfiguration.getPlaneHeight() + y);
					if (x > 0) {
						KohonenNeuron n = getNeurons().get((x - 1) * kohonenConfiguration.getPlaneHeight() + y);
						c.getNeighbours().add(n);
						n.getNeighbours().add(c);
					}
					if (y > 0) {
						KohonenNeuron n = getNeurons().get(x * kohonenConfiguration.getPlaneHeight() + y - 1);
						c.getNeighbours().add(n);
						n.getNeighbours().add(c);
					}
				}
			}
		} else {
			int x = 0;
			int y = 0;
			KohonenNeuron f = null;
			while (true) {
				KohonenNeuron c = getNeurons().get(x * kohonenConfiguration.getPlaneHeight() + y);
				if (f != null) {
					c.getNeighbours().add(f);
					f.getNeighbours().add(c);
				}
				if (x == kohonenConfiguration.getPlaneWidth() - 1 && y == kohonenConfiguration.getPlaneHeight() - 1) {
					break;
				}
				boolean phase = (x + y) % 2 == 0;
				if (phase) {
					if (x == kohonenConfiguration.getPlaneWidth() - 1) {
						++y;
					} else if (y == 0) {
						++x;
					} else {
						--y;
						++x;
					}
				} else {
					if (y == kohonenConfiguration.getPlaneHeight() - 1) {
						++x;
					} else if (x == 0) {
						++y;
					} else {
						++y;
						--x;
					}
				}
				f = c;
			}
		}
	}

	public void setKohonenConfiguration(KohonenConfiguration kohonenConfiguration) {
		this.kohonenConfiguration = kohonenConfiguration;
	}

	public KohonenConfiguration getKohonenConfiguration() {
		return kohonenConfiguration;
	}

	public void setNeurons(List<KohonenNeuron> neurons) {
		this.neurons = neurons;
	}

	public List<KohonenNeuron> getNeurons() {
		return neurons;
	}

	@Override
	public boolean isLearningFinished() {
		return learningRate < 0.0001 || currentStep > kohonenConfiguration.getMaxEpochs();
	}

	public int getEpoch() {
		return currentStep;
	}

}
